﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Linq;
using OperationPlugins.Compositions;
using OperationPlugins.ExtensionMethods;
using OperationPlugins.Handlers;

namespace OperationPlugins.Models
{
    public abstract class ModelBase : INotifyPropertyChanging, INotifyPropertyChanged, ICloneable
    {
        /// <summary>
        /// The property matcher.
        /// </summary>
        private static IPropertyMatcher PropertyMatcher = new DefaultPropertyMatcher();

        /// <summary>
        /// This dictionary contains all property values.
        /// </summary>
        private readonly ConcurrentDictionary<int, PropertyValue> _properties = new ConcurrentDictionary<int, PropertyValue>();

        /// <summary>
        /// For implementing the INotifyPropertyChanged interface.
        /// </summary>
        private event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// For implementing the INotifyPropertyChanging interface.
        /// </summary>
        private event PropertyChangingEventHandler PropertyChanging;

        /// <summary>
        /// Sets the property value. 
        /// </summary>
        /// <param name="declaringType">The type that declares the property.</param>
        /// <param name="propertyName">Property name.</param>
        /// <param name="propertyType">Property type.</param>
        /// <param name="value">Property value.</param>
        protected void SetPropertyValue(Type declaringType, string propertyName, Type propertyType, object value)
        {
            int hash = GetPropertyHash(declaringType, propertyName);

            // Try to get an existing property value.
            PropertyValue oldValue;
            bool oldFound = _properties.TryGetValue(hash, out oldValue);

            // Check if the property value is actually changing.
            bool changing = false;
            if (oldFound)
            {
                if (Object.Equals(value, oldValue.Value) == false)
                {
                    changing = true;
                }
            }
            else
            {
                if (value != null)
                {
                    changing = true;
                }
                else
                {
                    if (ObjectFactory.GetDefaultValue(propertyType) != null)
                    {
                        changing = true;
                    }
                }
            }

            // Exit if the property value is not changing!
            if (changing == false)
            {
                return;
            }

            // The new property value.
            PropertyValue newValue = new PropertyValue(declaringType, 
                                                       propertyName, 
                                                       declaringType.GetProperty(propertyName), 
                                                       propertyType, 
                                                       value);
            
            // Raise before-events.
            bool cancel = OnPropertyChanging(newValue, oldValue);

            // Exit if the property value changing was cancelled!
            if (cancel)
            {
                return;
            }

            // Set the property value.
            bool setSucceed;
            if (oldFound)
            {
                setSucceed = _properties.TryUpdate(hash, newValue, oldValue);
            }
            else
            {
                setSucceed = _properties.TryAdd(hash, newValue);
            }
            if (setSucceed == false)
            {
                throw new ModelConcurrencyException();
            }

            // Raise after-events.
            OnPropertyChanged(newValue, oldValue);
        }

        /// <summary>
        /// Gets the property value.
        /// </summary>
        /// <param name="declaringType">The type that declares the property.</param>
        /// <param name="propertyName">Property name.</param>
        /// <param name="propertyType">Property type.</param>
        /// <returns>The property value.</returns>
        protected object GetPropertyValue(Type declaringType, string propertyName, Type propertyType)
        {
            object value;

            bool gotExternal = InvokePropertyGetterHandlers(declaringType, propertyName, propertyType, out value);
            if (gotExternal)
            {
                return value;
            }

            int hash = GetPropertyHash(declaringType, propertyName);

            PropertyValue oldValue;
            bool oldFound = _properties.TryGetValue(hash, out oldValue);

            if (oldFound)
            {
                value = oldValue.Value;
            }
            else
            {
                value = ObjectFactory.GetDefaultValue(propertyType);
            }

            return value;
        }

        /// <summary>
        /// Notifies that a method has been invoked.
        /// </summary>
        /// <param name="declaringType"></param>
        /// <param name="methodSignature">The method signature.</param>
        /// <param name="parameters">Method parameters.</param>
        /// <returns></returns>
        protected MethodReturnValue NotifyMethodInvoked(Type declaringType, string methodSignature, MethodParameters parameters)
        {
         
            // InvokeMethod
            // InvokeMethodHandler
            // InvokeMethodAttribute
   

            var x = declaringType.GetMethods();
            throw new NotImplementedException();
        }

        /// <summary>
        /// Occurs just before the property value is changing.
        /// </summary>
        /// <param name="newValue">The new value of the property that is changing.</param>
        /// <param name="oldValue">The old value of the property that is changing.</param>
        /// <returns>true if the property changing should be cancelled, otherwise false.</returns>
        protected virtual bool OnPropertyChanging(PropertyValue newValue, PropertyValue oldValue)
        {
            // Invoke PropertyChangingAttribute implementations.
            foreach (var attribute in newValue.Property.GetAttributes<PropertyChangingAttribute>())
            {
                var cancel = attribute.PropertyChanging(this, newValue, oldValue);
                if (cancel)
                {
                    return true;
                }
            }

            // Raise PropertyChanging events.
            var e = new ModelPropertyChangingEventArgs(this, newValue, oldValue);
            OnPropertyChanging(this, e);
            if (e.Cancel)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Occurs after a property value has been changed.
        /// </summary>
        /// <param name="newValue">The new value of the property that changed.</param>
        /// <param name="oldValue">The old value of the property that changed.</param>
        protected virtual void OnPropertyChanged(PropertyValue newValue, PropertyValue oldValue)
        {
            // Invoke PropertyChangedAttribute implementations.
            foreach (var attribute in newValue.Property.GetAttributes<PropertyChangedAttribute>())
            {
                attribute.PropertyChanged(this, newValue, oldValue);
            }

            // Raise PropertyChanged events.
            var e1 = new ModelPropertyChangedEventArgs(this, newValue, oldValue);
            OnPropertyChanged(this, e1);

            // Raise PropertyChanged events for dependent properties.
            foreach (var property in newValue.DeclaringType.GetPublicProperties())
            {
                foreach (var depends in property.GetAttributes<PropertyValueDependsOnAttribute>())
                {
                    if (PropertyMatcher.Match(depends, newValue.DeclaringType, newValue.PropertyName))
                    {
                        var e2 = new PropertyChangedEventArgs(property.Name);
                        OnPropertyChanged(this, e2);
                    }
                }
            }

            // Invoke property setter handlers.
            InvokePropertySetterHandlers(newValue, oldValue);
        }

        /// <summary>
        /// Raises the PropertyChanging event.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">Provides data for the PropertyChanging event.</param>
        protected virtual void OnPropertyChanging(object sender, PropertyChangingEventArgs e)
        {
            var handlers = PropertyChanging;
            if (handlers != null)
            {
                handlers(this, e);
            }            
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">Provides data for the PropertyChanged event.</param>
        protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var handlers = PropertyChanged;
            if (handlers != null)
            {
                handlers(this, e);
            }
        }

        /// <summary>
        /// Resolves the current operation.
        /// </summary>
        /// <returns>The current operation.</returns>
        protected virtual OperationId ResolveOperation()
        {
            return OperationPluginsContext.Current.CurrentActionOperation ?? Operations.Any;
        }

        /// <summary>
        /// Resolves the current branch.
        /// </summary>
        /// <returns>The current branch.</returns>
        protected virtual BranchId ResolveBranch()
        {
            return OperationPluginsContext.Current.CurrentActionBranch ?? Branches.Any;
        }

        /// <summary>
        /// Resolves the operation context factory.
        /// </summary>
        /// <returns>The operation context factory.</returns>
        protected virtual IOperationContextFactory ResolveOperationContextFactory()
        {
            return OperationContextManager.GetOperationContextFactory();
        }

        /// <summary>
        /// Invokes property setter handlers.
        /// </summary>
        /// <param name="newValue">The new value of the property that changed.</param>
        /// <param name="oldValue">The old value of the property that changed.</param>
        private void InvokePropertySetterHandlers(PropertyValue newValue, PropertyValue oldValue)
        {
            var envelope = new SetPropertyValueEnvelope(this, newValue, oldValue);
            var operation = ResolveOperation();
            var branch = ResolveBranch();
            var contextFactory = ResolveOperationContextFactory();

            Handler.Invoke(
                envelope,
                operation,
                branch,
                contextFactory,
                (handlerType => MatchFilter<SetPropertyValueAttribute>(handlerType, newValue.DeclaringType, newValue.PropertyName)));
        }

        private bool InvokePropertyGetterHandlers(Type declaringType, string propertyName, Type propertyType, out object value)
        {
            value = null;

            var envelope = new GetPropertyValueEnvelope(this, declaringType, propertyName);
            var operation = ResolveOperation();
            var branch = ResolveBranch();
            var contextFactory = ResolveOperationContextFactory();

            Handler.Invoke(
                envelope,
                operation,
                branch,
                contextFactory,
                (handlerType => MatchFilter<GetPropertyValueAttribute>(handlerType, declaringType, propertyName)));

            if (envelope.ReturnValues.Count == 0)
            {
                return false;
            }

            if (envelope.ReturnValues.Count > 1)
            {
                throw new AmbiguousPropertyGetterReturnValueException("More than one return values were found.");
            }

            var v = envelope.ReturnValues.First();

            if (v == null)
            {
                if (ObjectFactory.GetDefaultValue(propertyType) != null)
                {
                    throw new InvalidTypeOfPropertyValueException("The property return value cannot be null.");
                }
            }
            else
            {
                if (propertyType.IsInstanceOfType(v) == false)
                {
                    throw new InvalidTypeOfPropertyValueException("The type of the property return value is invalid.");
                }
            }

            value = v;

            return true;
        }

        /// <summary>
        /// Gets the hash for the property.
        /// </summary>
        /// <param name="declaringType">The type that declares the property.</param>
        /// <param name="propertyName">The property name.</param>
        /// <returns>The hash.</returns>
        private static int GetPropertyHash(Type declaringType, string propertyName)
        {
            return string.Format("{0}|{1}", declaringType.GetFullName(), propertyName).GetHashCode();
        }

        private static bool MatchFilter<TAttribute>(Type handlerType, Type declaringType, string propertyName) where TAttribute : Attribute
        {
            foreach (TAttribute attribute in handlerType.GetAttributes<TAttribute>())
            {
                if (PropertyMatcher.Match((IPropertyFilter)attribute, declaringType, propertyName))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Occurs when a property value is changing.
        /// </summary>
        event PropertyChangingEventHandler INotifyPropertyChanging.PropertyChanging
        {
            add
            {
                PropertyChanging += value;
            }
            remove
            {
                PropertyChanging -= value;
            }
        }

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>        
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add
            {
                PropertyChanged += value;
            }
            remove
            {
                PropertyChanged -= value;
            }
        }

        /// <summary>
        /// Creates a new instance (object) that is a copy of the current object. 
        /// </summary>
        /// <returns>A new object that is a copy of the current object.</returns>
        object ICloneable.Clone()
        {
            // Create a new instance of the class.
            ModelBase clone = (ModelBase)ObjectFactory.CreateClassInstance(GetType());

            // Clone each property value.
            foreach (KeyValuePair<int, PropertyValue> property in _properties)
            {
                PropertyValue valueClone = (PropertyValue)property.Value.Clone();

                bool succeed = clone._properties.TryAdd(property.Key, valueClone);
                if (succeed == false)
                {
                    throw new InvalidOperationException("Failed to clone property value.");
                }
            }

            return clone;
        }

        /// <summary>
        /// Initialize.
        /// </summary>
        [Export(typeof(IInitializable))]
        private sealed class Initializer : IInitializable
        {
            void IInitializable.Initialize(InitializationContext context)
            {
                PropertyMatcher = context.CompositionContainer.GetPlugin<IPropertyMatcher>();
            }
        }

    }
}
